1 /** 2 Code to parse the output from `dub describe` and generate the main 3 test file automatically. 4 */ 5 module unit_threaded.dub; 6 7 import unit_threaded.from; 8 9 struct DubPackage { 10 string name; 11 string path; 12 string mainSourceFile; 13 string targetFileName; 14 string[] flags; 15 string[] importPaths; 16 string[] stringImportPaths; 17 string[] files; 18 string targetType; 19 string[] versions; 20 string[] dependencies; 21 string[] libs; 22 bool active; 23 } 24 25 struct DubInfo { 26 DubPackage[] packages; 27 } 28 29 DubInfo getDubInfo(string jsonString) @trusted { 30 import std.json : parseJSON; 31 import std.algorithm : map, filter; 32 import std.array : array; 33 34 auto json = parseJSON(jsonString); 35 auto packages = json.byKey("packages").array; 36 return DubInfo(packages.map!(a => DubPackage(a.byKey("name").str, 37 a.byKey("path").str, a.getOptional("mainSourceFile"), 38 a.getOptional("targetFileName"), a.byKey("dflags").jsonValueToStrings, a.byKey("importPaths") 39 .jsonValueToStrings, a.byKey("stringImportPaths").jsonValueToStrings, a.byKey("files") 40 .jsonValueToFiles, a.getOptional("targetType"), a.getOptionalList("versions"), 41 a.getOptionalList("dependencies"), a.getOptionalList("libs"), 42 a.byOptionalKey("active", true), //true for backwards compatibility 43 )).filter!(a => a.active).array); 44 } 45 46 private string[] jsonValueToFiles(from!"std.json".JSONValue files) @trusted { 47 import std.algorithm : map, filter; 48 import std.array : array; 49 50 return files.array.filter!(a => ("type" in a && a.byKey("type") 51 .str == "source") || ("role" in a && a.byKey("role").str == "source") 52 || ("type" !in a && "role" !in a)).map!(a => a.byKey("path").str).array; 53 } 54 55 private string[] jsonValueToStrings(from!"std.json".JSONValue json) @trusted { 56 import std.algorithm : map, filter; 57 import std.array : array; 58 59 return json.array.map!(a => a.str).array; 60 } 61 62 private auto byKey(from!"std.json".JSONValue json, in string key) @trusted { 63 import std.json : JSONException; 64 65 if (auto p = key in json.object) 66 return *p; 67 else 68 throw new JSONException("\"" ~ key ~ "\" not found"); 69 } 70 71 private auto byOptionalKey(from!"std.json".JSONValue json, in string key, bool def) { 72 if (auto p = key in json.object) 73 return (*p).boolean; 74 else 75 return def; 76 } 77 78 //std.json has no conversion to bool 79 private bool boolean(from!"std.json".JSONValue json) @trusted { 80 import std.exception : enforce; 81 import std.json : JSONException, JSON_TYPE; 82 83 enforce!JSONException(json.type == JSON_TYPE.TRUE 84 || json.type == JSON_TYPE.FALSE, "JSONValue is not a boolean"); 85 return json.type == JSON_TYPE.TRUE; 86 } 87 88 private string getOptional(from!"std.json".JSONValue json, in string key) @trusted { 89 if (auto p = key in json.object) 90 return p.str; 91 else 92 return ""; 93 } 94 95 private string[] getOptionalList(from!"std.json".JSONValue json, in string key) @trusted { 96 if (auto p = key in json.object) 97 return (*p).jsonValueToStrings; 98 else 99 return []; 100 } 101 102 DubInfo getDubInfo(in bool verbose) { 103 import std.json : JSONException; 104 import std.conv : text; 105 import std.algorithm : joiner, map, copy; 106 import std.stdio : writeln; 107 import std.exception : enforce; 108 import std.process : pipeProcess, Redirect, wait; 109 import std.array : join, appender; 110 111 if (verbose) 112 writeln("Running dub describe"); 113 114 immutable args = ["dub", "describe", "-c", "unittest"]; 115 auto pipes = pipeProcess(args, Redirect.stdout | Redirect.stderr); 116 scope (exit) 117 wait(pipes.pid); // avoid zombies in all cases 118 string stdoutStr; 119 string stderrStr; 120 enum chunkSize = 4096; 121 pipes.stdout.byChunk(chunkSize).joiner.map!"cast(immutable char)a".copy(appender(&stdoutStr)); 122 pipes.stderr.byChunk(chunkSize).joiner.map!"cast(immutable char)a".copy(appender(&stderrStr)); 123 auto status = wait(pipes.pid); 124 auto allOutput = "stdout:\n" ~ stdoutStr ~ "\nstderr:\n" ~ stderrStr; 125 126 enforce(status == 0, text("Could not execute ", args.join(" "), ":\n", allOutput)); 127 try { 128 return getDubInfo(stdoutStr); 129 } catch (JSONException e) { 130 throw new Exception(text("Could not parse the output of dub describe:\n", 131 allOutput, "\n", e.toString)); 132 } 133 } 134 135 bool isDubProject() { 136 import std.file; 137 138 return "dub.sdl".exists || "dub.json".exists || "package.json".exists; 139 } 140 141 // set import paths from dub information 142 void dubify(ref from!"unit_threaded.runtime".Options options) { 143 144 import std.path : buildPath; 145 import std.algorithm : map, reduce; 146 import std.array : array; 147 148 if (!isDubProject) 149 return; 150 151 auto dubInfo = getDubInfo(options.verbose); 152 options.includes = dubInfo.packages.map!( 153 a => a.importPaths.map!(b => buildPath(a.path, b)).array).reduce!((a, b) => a ~ b) 154 .array; 155 options.files = dubInfo.packages[0].files; 156 }